package org.appspot.apprtc;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.Logging;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RtpParameters;
import org.webrtc.RtpReceiver;
import org.webrtc.RtpSender;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.StatsObserver;
import org.webrtc.StatsReport;
import org.webrtc.PeerConnection.IceConnectionState;
import org.webrtc.PeerConnection.TlsCertPolicy;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoTrack;

import android.util.Log;

public class MRTCPeerConnection {

	  private static final String TAG = "MRTCPeerConnection";
	  
	  private LocalMediaStream local_media;
	  private VideoRenderer.Callbacks remote_render;
	  private VideoTrack remoteVideoTrack;
	  private boolean enableVideoTrack;
	  private boolean isError;
	  private PeerConnection peerConnection;
	  private SessionDescription localSdp; // either offer or answer SDP
	  private final SDPObserver sdpObserver = new SDPObserver();
	  private final PCObserver pcObserver = new PCObserver();
	  public PeerConnectionEvents events;
	  private RtpSender localVideoSender;
	  private String peerid;
	  private String rtrole;
	  private Timer statsTimer;
	  
	  private final ScheduledExecutorService executor;
      
      
	// Queued remote ICE candidates are consumed only after both local and
	// remote descriptions are set. Similarly local ICE candidates are sent to
	// remote peer after both local and remote description are set.
	private LinkedList<IceCandidate> queuedRemoteCandidates;
	  
	public MRTCPeerConnection(LocalMediaStream local_media, final VideoRenderer.Callbacks remoteRender, 
			String peerid, String rtrole,PeerConnectionEvents eventListener) {
		executor = Executors.newSingleThreadScheduledExecutor();
		this.peerid = peerid;
		this.rtrole = rtrole;
		this.events = eventListener;
		this.local_media = local_media;
		this.remote_render = remoteRender;
		statsTimer = new Timer();
		enableVideoTrack = true;
		isError = false;
		createPeerConnection();	
	}
	
	
  //////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////
  /////////////////////////PeerConnection///////////////////////	
  private void createPeerConnection() {
	  if (local_media==null || local_media.factory == null || isError) {
	    Log.e(TAG, "Peerconnection factory is not created");
	    return;
	  }
	    
      Log.d(TAG, "start to create peer connection.");
      Log.d(TAG, "PCConstraints: " + local_media.pcConstraints.toString());

	  queuedRemoteCandidates = new LinkedList<IceCandidate>();
		      
	  PeerConnection.RTCConfiguration rtcConfig =
		        new PeerConnection.RTCConfiguration(createICEServers());
	  
	  // TCP candidates are only useful when connecting to a server that supports
	  // ICE-TCP.
	  rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
	  rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
	  rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
	  rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
	  // Use ECDSA encryption.
	  rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
	  
	  peerConnection = LocalMediaStream.factory.createPeerConnection(rtcConfig, local_media.pcConstraints, pcObserver);
 
	  // Set default WebRTC tracing and INFO libjingle logging.
	  // NOTE: this _must_ happen while |factory| is alive!
	  Logging.enableTracing("logcat:", EnumSet.of(Logging.TraceLevel.TRACE_DEFAULT));
	  Logging.enableLogToDebugOutput(Logging.Severity.LS_INFO);
	  
	  peerConnection.addStream(local_media.local_stream);
	  
	  if (local_media.videoCallEnabled) {
	      findVideoSender();
	  }
	  
	  Log.d(TAG, "Peer connection created.");
  }
	
  private List<PeerConnection.IceServer> createICEServers(){
	List<PeerConnection.IceServer> iceservers = new ArrayList<PeerConnection.IceServer>();
	
	iceservers.add(new PeerConnection.IceServer(
			"turn:120.25.94.58:3478?transport=udp", "skyrtc", "skyrtc")
			);
	
	iceservers.add(new PeerConnection.IceServer(
			"turn:120.25.94.58:3478?transport=tcp", "skyrtc", "skyrtc")
			);
	
	iceservers.add(new PeerConnection.IceServer(
			"stun:120.25.94.58:3478")
			);
	
	return iceservers;
  }
	
  private void findVideoSender() {
    for (RtpSender sender : peerConnection.getSenders()) {
      if (sender.track() != null) {
        String trackType = sender.track().kind();
        if (trackType.equals(LocalMediaStream.VIDEO_TRACK_TYPE)) {
          Log.d(TAG, "Found video sender.");
          localVideoSender = sender;
        }
      }
    }
  }

  // Implementation detail: observe ICE & stream changes and react accordingly.
  private class PCObserver implements PeerConnection.Observer {
	 
	////////////////////////////////local events/////////////////////////////////
	//need to send msg to other participant	  
	/** Triggered when a new ICE candidate has been found. */
	// Send Ice candidate to the other participant. 
    @Override
    public void onIceCandidate(final IceCandidate candidate) {
      executor.execute(new Runnable() {
        @Override
        public void run() {      	
          events.onIceCandidate(peerid, candidate);
        }
      });
    }

    /** Triggered when some ICE candidates have been removed. */
    // Send removed Ice candidates to the other participant.
    @Override
    public void onIceCandidatesRemoved(final IceCandidate[] candidates) {
      executor.execute(new Runnable() {
        @Override
        public void run() {     
          events.onIceCandidatesRemoved(peerid,candidates);
        }
      });
    }

      
	////////////////////////////local status changes event///////////////////////////
    //SignalingState
    @Override
    public void onSignalingChange(PeerConnection.SignalingState newState) {
      Log.d(TAG, "SignalingState: " + newState);
    }
    //IceConnectionState
    @Override
    public void onIceConnectionChange(final PeerConnection.IceConnectionState newState) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          Log.d(TAG, "IceConnectionState: " + newState);
          if (newState == IceConnectionState.CONNECTED) {
            events.onIceConnected(peerid);
          } else if (newState == IceConnectionState.DISCONNECTED) {
            events.onIceDisconnected();
          } else if (newState == IceConnectionState.FAILED) {
            reportError("ICE connection failed.");
          }
        }
      });
    }
    //IceGatheringState
    @Override
    public void onIceGatheringChange(PeerConnection.IceGatheringState newState) {
      Log.d(TAG, "IceGatheringState: " + newState);
    }
    //ICE connection receiving status
    @Override
    public void onIceConnectionReceivingChange(boolean receiving) {
      Log.d(TAG, "IceConnectionReceiving changed to " + receiving);
    }

    
    ////////////////////////////remote stream events///////////////////////////
    /** Triggered when media is received on a new stream from remote peer. */    
    @Override
    public void onAddStream(final MediaStream stream) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          if (peerConnection == null || isError) {
            return;
          }
          if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) {
            reportError("Weird-looking stream: " + stream);
            return;
          }
          if (stream.videoTracks.size() == 1) {
            remoteVideoTrack = stream.videoTracks.get(0);
            remoteVideoTrack.setEnabled(enableVideoTrack);
            remoteVideoTrack.addRenderer(new VideoRenderer(remote_render));
          }
        }
      });
    }

    /** Triggered when a remote peer close a stream. */
    @Override
    public void onRemoveStream(final MediaStream stream) {
      executor.execute(new Runnable() {
        @Override
        public void run() {
          remoteVideoTrack = null;
        }
      });
    }

    
    
    /** Triggered when a remote peer opens a DataChannel. */
    @Override
    public void onDataChannel(final DataChannel dc) {
      Log.d(TAG, "New Data channel " + dc.label());
    }

    /** Triggered when renegotiation is necessary. */
    @Override
    public void onRenegotiationNeeded() {
      // No need to do anything; AppRTC follows a pre-agreed-upon
      // signaling/negotiation protocol.
    }

    /** Triggered when a new track is signaled by the remote peer, as a result of setRemoteDescription.*/
    @Override
    public void onAddTrack(final RtpReceiver receiver, final MediaStream[] mediaStreams) {}
  }
  
  ///////////////////////////ICE operation/////////////////////////////////
  public void addRemoteIceCandidate(final IceCandidate candidate) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (peerConnection != null && !isError) {
          if (queuedRemoteCandidates != null) {
            queuedRemoteCandidates.add(candidate);
          } else {
            peerConnection.addIceCandidate(candidate);
          }
        }
      }
    });
  }

  public void removeRemoteIceCandidates(final IceCandidate[] candidates) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (peerConnection == null || isError) {
          return;
        }
        // Drain the queued remote candidates if there is any so that
        // they are processed in the proper order.
        drainCandidates();
        peerConnection.removeIceCandidates(candidates);
      }
    });
  }
  
  private void drainCandidates() {
    if (queuedRemoteCandidates != null) {
      Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
      for (IceCandidate candidate : queuedRemoteCandidates) {
        peerConnection.addIceCandidate(candidate);
      }
      queuedRemoteCandidates = null;
    }
  }
	  
	  

  

  //////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////
  ////////////////////////////SDP///////////////////////////////
  public void createOffer() {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (peerConnection != null && !isError) {
          Log.d(TAG, "PC Create OFFER");
          peerConnection.createOffer(sdpObserver, local_media.sdpMediaConstraints);
        }
      }
    });
  }

  public void createAnswer() {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (peerConnection != null && !isError) {
          Log.d(TAG, "PC create ANSWER");        
          peerConnection.createAnswer(sdpObserver, local_media.sdpMediaConstraints);
        }
      }
    });
  }
	
  public void setRemoteDescription(final SessionDescription sdp) {
	    executor.execute(new Runnable() {
	      @Override
	      public void run() {
	        if (peerConnection == null || isError) {
	          return;
	        }
	        String sdpDescription = sdp.description;
	        if (local_media.preferIsac) {
	          sdpDescription = preferCodec(sdpDescription, LocalMediaStream.AUDIO_CODEC_ISAC, true);
	        }
	        if (local_media.videoCallEnabled) {
	          sdpDescription = preferCodec(sdpDescription, local_media.preferredVideoCodec, false);
	        }
	        if (local_media.audioStartBitrate > 0) {
	          sdpDescription = setStartBitrate(
	        		  LocalMediaStream.AUDIO_CODEC_OPUS, false, sdpDescription, local_media.audioStartBitrate);
	        }
	        Log.d(TAG, "Set remote SDP.");
	        SessionDescription sdpRemote = new SessionDescription(sdp.type, sdpDescription);
	        peerConnection.setRemoteDescription(sdpObserver, sdpRemote);
	      }
	    });
	  }
	  
  private static String setStartBitrate(String codec, boolean isVideoCodec, String sdpDescription, int bitrateKbps) {
    String[] lines = sdpDescription.split("\r\n");
    int rtpmapLineIndex = -1;
    boolean sdpFormatUpdated = false;
    String codecRtpMap = null;
    // Search for codec rtpmap in format
    // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
    String regex = "^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$";
    Pattern codecPattern = Pattern.compile(regex);
    for (int i = 0; i < lines.length; i++) {
      Matcher codecMatcher = codecPattern.matcher(lines[i]);
      if (codecMatcher.matches()) {
        codecRtpMap = codecMatcher.group(1);
        rtpmapLineIndex = i;
        break;
      }
    }
    if (codecRtpMap == null) {
      Log.w(TAG, "No rtpmap for " + codec + " codec");
      return sdpDescription;
    }
    Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap + " at " + lines[rtpmapLineIndex]);

    // Check if a=fmtp string already exist in remote SDP for this codec and
    // update it with new bitrate parameter.
    regex = "^a=fmtp:" + codecRtpMap + " \\w+=\\d+.*[\r]?$";
    codecPattern = Pattern.compile(regex);
    for (int i = 0; i < lines.length; i++) {
      Matcher codecMatcher = codecPattern.matcher(lines[i]);
      if (codecMatcher.matches()) {
        Log.d(TAG, "Found " + codec + " " + lines[i]);
        if (isVideoCodec) {
          lines[i] += "; " + LocalMediaStream.VIDEO_CODEC_PARAM_START_BITRATE + "=" + bitrateKbps;
        } else {
          lines[i] += "; " + LocalMediaStream.AUDIO_CODEC_PARAM_BITRATE + "=" + (bitrateKbps * 1000);
        }
        Log.d(TAG, "Update remote SDP line: " + lines[i]);
        sdpFormatUpdated = true;
        break;
      }
    }

    StringBuilder newSdpDescription = new StringBuilder();
    for (int i = 0; i < lines.length; i++) {
      newSdpDescription.append(lines[i]).append("\r\n");
      // Append new a=fmtp line if no such line exist for a codec.
      if (!sdpFormatUpdated && i == rtpmapLineIndex) {
        String bitrateSet;
        if (isVideoCodec) {
          bitrateSet =
              "a=fmtp:" + codecRtpMap + " " + LocalMediaStream.VIDEO_CODEC_PARAM_START_BITRATE + "=" + bitrateKbps;
        } else {
          bitrateSet = "a=fmtp:" + codecRtpMap + " " + LocalMediaStream.AUDIO_CODEC_PARAM_BITRATE + "="
              + (bitrateKbps * 1000);
        }
        Log.d(TAG, "Add remote SDP line: " + bitrateSet);
        newSdpDescription.append(bitrateSet).append("\r\n");
      }
    }
    return newSdpDescription.toString();
  }
	  
	  
  // Implementation detail: handle offer creation/signaling and answer setting,
  // as well as adding remote ICE candidates once the answer SDP is set.
  private class SDPObserver implements SdpObserver {
	  
	/** Called on success of Create{Offer,Answer}(). */ 
    @Override
    public void onCreateSuccess(final SessionDescription origSdp) {
      if (localSdp != null) {
        reportError("Multiple SDP create.");
        return;
      }
      String sdpDescription = origSdp.description;
      if (local_media.preferIsac) {
        sdpDescription = preferCodec(sdpDescription, LocalMediaStream.AUDIO_CODEC_ISAC, true);
      }
      if (local_media.videoCallEnabled) {
        sdpDescription = preferCodec(sdpDescription, local_media.preferredVideoCodec, false);
      }
      final SessionDescription sdp = new SessionDescription(origSdp.type, sdpDescription);
      localSdp = sdp;
      executor.execute(new Runnable() {
        @Override
        public void run() {
          if (peerConnection != null && !isError) {
            Log.d(TAG, "Set local SDP from " + sdp.type);
            peerConnection.setLocalDescription(sdpObserver, sdp);
          }
        }
      });
    }

    /** Called on success of Set{Local,Remote}Description(). */
    @Override
    public void onSetSuccess() {
      executor.execute(new Runnable() {
        @Override
        public void run() {
        	 
          if (peerConnection == null || isError) {
            return;
          }
          
          if(rtrole.equals("participant")) {// local is initiator relative to the remote
            // For offering peer connection we first create offer and set
            // local SDP, then after receiving answer set remote SDP.
            if (peerConnection.getRemoteDescription() == null) {
              // We've just set our local SDP so time to send it.
              Log.d(TAG, "Local OFFER SDP set succesfully, can be send to remote.");
              events.onLocalDescription(peerid,localSdp);
            } else {
              // We've just set remote description, so drain remote
              // and send local ICE candidates.
              Log.d(TAG, "Remote answer SDP set succesfully");
              drainCandidates();
            }
          } else {// local is participant relative to the remote
            // For answering peer connection we set remote SDP and then
            // create answer and set local SDP.
            if (peerConnection.getLocalDescription() != null) {
              // We've just set our local SDP so time to send it, drain
              // remote and send local ICE candidates.
              Log.d(TAG, "Local answer SDP set succesfully, can be send to remote");
              events.onLocalDescription(peerid,localSdp);
              drainCandidates();
            } else {
              // We've just set remote SDP - do nothing for now -
              // answer will be created soon.
              Log.d(TAG, "Remote  Offer SDP set succesfully");
            }
          }
        }
      });
    }

    /** Called on error of Create{Offer,Answer}(). */
    @Override
    public void onCreateFailure(final String error) {
      reportError("createSDP error: " + error);
    }

    /** Called on error of Set{Local,Remote}Description(). */
    @Override
    public void onSetFailure(final String error) {
      reportError("setSDP error: " + error);
    }
  }
  
  /** Returns the line number containing "m=audio|video", or -1 if no such line exists. */
  private static int findMediaDescriptionLine(boolean isAudio, String[] sdpLines) {
    final String mediaDescription = isAudio ? "m=audio " : "m=video ";
    for (int i = 0; i < sdpLines.length; ++i) {
      if (sdpLines[i].startsWith(mediaDescription)) {
        return i;
      }
    }
    return -1;
  }
  private static String movePayloadTypesToFront(List<String> preferredPayloadTypes, String mLine) {
	    // The format of the media description line should be: m=<media> <port> <proto> <fmt> ...
	    final List<String> origLineParts = Arrays.asList(mLine.split(" "));
	    if (origLineParts.size() <= 3) {
	      Log.e(TAG, "Wrong SDP media description format: " + mLine);
	      return null;
	    }
	    final List<String> header = origLineParts.subList(0, 3);
	    final List<String> unpreferredPayloadTypes =
	        new ArrayList<String>(origLineParts.subList(3, origLineParts.size()));
	    unpreferredPayloadTypes.removeAll(preferredPayloadTypes);
	    // Reconstruct the line with |preferredPayloadTypes| moved to the beginning of the payload
	    // types.
	    final List<String> newLineParts = new ArrayList<String>();
	    newLineParts.addAll(header);
	    newLineParts.addAll(preferredPayloadTypes);
	    newLineParts.addAll(unpreferredPayloadTypes);
	    return joinString(newLineParts, " ", false /* delimiterAtEnd */);
  }
  private static String joinString(Iterable<? extends CharSequence> s, String delimiter, boolean delimiterAtEnd) {
	    Iterator<? extends CharSequence> iter = s.iterator();
	    if (!iter.hasNext()) {
	      return "";
	    }
	    StringBuilder buffer = new StringBuilder(iter.next());
	    while (iter.hasNext()) {
	      buffer.append(delimiter).append(iter.next());
	    }
	    if (delimiterAtEnd) {
	      buffer.append(delimiter);
	    }
	    return buffer.toString();
  }
  private static String preferCodec(String sdpDescription, String codec, boolean isAudio) {
	    final String[] lines = sdpDescription.split("\r\n");
	    final int mLineIndex = findMediaDescriptionLine(isAudio, lines);
	    if (mLineIndex == -1) {
	      Log.w(TAG, "No mediaDescription line, so can't prefer " + codec);
	      return sdpDescription;
	    }
	    // A list with all the payload types with name |codec|. The payload types are integers in the
	    // range 96-127, but they are stored as strings here.
	    final List<String> codecPayloadTypes = new ArrayList<String>();
	    // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
	    final Pattern codecPattern = Pattern.compile("^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$");
	    for (int i = 0; i < lines.length; ++i) {
	      Matcher codecMatcher = codecPattern.matcher(lines[i]);
	      if (codecMatcher.matches()) {
	        codecPayloadTypes.add(codecMatcher.group(1));
	      }
	    }
	    if (codecPayloadTypes.isEmpty()) {
	      Log.w(TAG, "No payload types with name " + codec);
	      return sdpDescription;
	    }

	    final String newMLine = movePayloadTypesToFront(codecPayloadTypes, lines[mLineIndex]);
	    if (newMLine == null) {
	      return sdpDescription;
	    }
	    Log.d(TAG, "Change media description from: " + lines[mLineIndex] + " to " + newMLine);
	    lines[mLineIndex] = newMLine;
	    return joinString(Arrays.asList(lines), "\r\n", true /* delimiterAtEnd */);
  }
  
  
  
  
 
  private void reportError(final String errorMessage) {
    Log.e(TAG, "Peerconnection error: " + errorMessage);
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (!isError) {
          events.onPeerConnectionError(errorMessage);
          isError = true;
        }
      }
    });
  }
	  
  
  public void setSendVideoMaxBitrate(final Integer maxBitrateKbps) {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        if (peerConnection == null || localVideoSender == null || isError) {
          return;
        }
        Log.d(TAG, "Requested max video bitrate: " + maxBitrateKbps);
        if (localVideoSender == null) {
          Log.w(TAG, "Sender is not ready.");
          return;
        }

        RtpParameters parameters = localVideoSender.getParameters();
        if (parameters.encodings.size() == 0) {
          Log.w(TAG, "RtpParameters are not ready.");
          return;
        }

        for (RtpParameters.Encoding encoding : parameters.encodings) {
          // Null value means no limit.
          encoding.maxBitrateBps = maxBitrateKbps == null ? null : maxBitrateKbps * LocalMediaStream.BPS_IN_KBPS;
        }
        if (!localVideoSender.setParameters(parameters)) {
          Log.e(TAG, "RtpSender.setParameters failed.");
        }
        Log.d(TAG, "Configured max video bitrate to: " + maxBitrateKbps);
      }
    });
  }
  
  
  public void enableStatsEvents(boolean enable, int periodMs) {
    if (enable) {
      try {
        statsTimer.schedule(new TimerTask() {
          @Override
          public void run() {
            executor.execute(new Runnable() {
              @Override
              public void run() {
                getStats();
              }
            });
          }
        }, 0, periodMs);
      } catch (Exception e) {
        Log.e(TAG, "Can not schedule statistics timer", e);
      }
    } else {
      statsTimer.cancel();
    }
  }
  
  private void getStats() {
    if (peerConnection == null || isError) {
      return;
    }
    boolean success = peerConnection.getStats(new StatsObserver() {
      @Override
      public void onComplete(final StatsReport[] reports) {
        events.onPeerConnectionStatsReady(peerid,reports);
      }
    }, null);
    if (!success) {
      Log.e(TAG, "getStats() returns false!");
    }
 }
  
  
  
  public void close() {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        closeInternal();
      }
    });
  }
  
  private void closeInternal() {
 
	statsTimer.cancel();
	  
    if (peerConnection != null) {
      peerConnection.dispose();
      peerConnection = null;
    }
    remote_render= null;
    events.onPeerConnectionClosed();
    events = null;
  }
  
  
	  
  /**
   * Peer connection events.
   */
  public interface PeerConnectionEvents {
    /**
     * Callback fired once local SDP is created and set.
     */
    void onLocalDescription(final String peerid, final SessionDescription sdp);

    /**
     * Callback fired once local Ice candidate is generated.
     */
    void onIceCandidate(final String peerid, final IceCandidate candidate);

    /**
     * Callback fired once local ICE candidates are removed.
     */
    void onIceCandidatesRemoved(final String peerid,final IceCandidate[] candidates);

    /**
     * Callback fired once connection is established (IceConnectionState is
     * CONNECTED).
     */
    void onIceConnected(final String peerid);

    /**
     * Callback fired once connection is closed (IceConnectionState is
     * DISCONNECTED).
     */
    void onIceDisconnected();

    /**
     * Callback fired once peer connection is closed.
     */
    void onPeerConnectionClosed();

    /**
     * Callback fired once peer connection statistics is ready.
     */
    void onPeerConnectionStatsReady(final String peerid,final StatsReport[] reports);

    /**
     * Callback fired once peer connection error happened.
     */
    void onPeerConnectionError(final String description);
    
  
  }
	  
	  
	  
	  
	  
	 
	
}
